Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /** * API route for listing per-problem vision recording videos for a session * * GET /api/curriculum/[playerId]/sessions/[sessionId]/videos * * Returns a list of available problem videos for the session. */ export const dynamic = 'force-dynamic' import { and, asc, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { db } from '@/db' import { sessionPlans, visionProblemVideos } from '@/db/schema' import { withAuth } from '@/lib/auth/withAuth' import { generateAuthorizationError, getPlayerAccess } from '@/lib/classroom' import { getUserId } from '@/lib/viewer' /** * GET - List available problem videos for a session */ export const GET = withAuth(async (_request, { params }) => { try { const { playerId, sessionId } = (await params) as { playerId: string; sessionId: string } if (!playerId || !sessionId) { return NextResponse.json({ error: 'Player ID and Session ID required' }, { status: 400 }) } // Authorization check const userId = await getUserId() const access = await getPlayerAccess(userId, playerId) if (access.accessLevel === 'none') { const authError = generateAuthorizationError(access, 'view', { actionDescription: 'view recordings for this student', }) return NextResponse.json(authError, { status: 403 }) } // Verify session exists and belongs to player const session = await db.query.sessionPlans.findFirst({ where: and(eq(sessionPlans.id, sessionId), eq(sessionPlans.playerId, playerId)), }) if (!session) { return NextResponse.json({ error: 'Session not found' }, { status: 404 }) } // Get all problem videos for this session (including failed/processing) // We'll dedupe and show the best status for each problem/epoch/attempt combo const rawVideos = await db.query.visionProblemVideos.findMany({ where: eq(visionProblemVideos.sessionId, sessionId), orderBy: [ asc(visionProblemVideos.problemNumber), asc(visionProblemVideos.epochNumber), asc(visionProblemVideos.attemptNumber), ], }) // Detect orphaned recordings: any 'recording' status video that has a newer // video in the same session is orphaned (we've moved on to another problem). // The only legitimate 'recording' is the one with the latest startedAt. const maxStartedAt = Math.max(...rawVideos.map((v) => v.startedAt.getTime())) const videos = rawVideos.map((video) => { if (video.status === 'recording' && video.startedAt.getTime() < maxStartedAt) { // This recording was abandoned - treat as no_video return { ...video, status: 'no_video' as const } } return video }) // Deduplicate by problem/epoch/attempt, keeping the "best" status // Priority: ready > processing > recording > no_video > failed const statusPriority: Record<string, number> = { ready: 0, processing: 1, recording: 2, no_video: 3, failed: 4, } const videoMap = new Map<string, (typeof videos)[0]>() for (const video of videos) { const key = `${video.problemNumber}-${video.epochNumber}-${video.attemptNumber}` const existing = videoMap.get(key) if (!existing) { videoMap.set(key, video) } else { // Keep the one with better status (lower priority number) const existingPriority = statusPriority[existing.status] ?? 999 const currentPriority = statusPriority[video.status] ?? 999 if (currentPriority < existingPriority) { videoMap.set(key, video) } } } // Convert map to sorted array const dedupedVideos = Array.from(videoMap.values()).sort((a, b) => { if (a.problemNumber !== b.problemNumber) return a.problemNumber - b.problemNumber if (a.epochNumber !== b.epochNumber) return a.epochNumber - b.epochNumber return a.attemptNumber - b.attemptNumber }) // Transform to response format with epoch/attempt info const videoList = dedupedVideos.map((video) => ({ problemNumber: video.problemNumber, partIndex: video.partIndex, epochNumber: video.epochNumber, attemptNumber: video.attemptNumber, isRetry: video.isRetry, isManualRedo: video.isManualRedo, status: video.status, durationMs: video.durationMs, fileSize: video.fileSize, isCorrect: video.isCorrect, startedAt: video.startedAt, endedAt: video.endedAt, processingError: video.processingError, })) return NextResponse.json({ videos: videoList }) } catch (error) { console.error('Error listing session videos:', error) return NextResponse.json({ error: 'Failed to list videos' }, { status: 500 }) } }) |